/* Copyright 2013 (C) FatalEror / Dean Bahari
 * 
 * jroCK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * See LICENSE file in main folder for more information.
 */

package jrock.filesystem;

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.ARBVertexBufferObject.*;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import jrock.other.Utility;
import org.lwjgl.BufferUtils;
import org.lwjgl.util.vector.Matrix4f;
import org.lwjgl.util.vector.Vector2f;
import org.lwjgl.util.vector.Vector3f;

/**
 * @author FatalEror
 */
public class RSM {
    
    private final String  magicKey = "GRSM";
    private int           version  = 0;
    private BMP[]         texture  = null;
    private Mesh[]        mesh     = null;
    
    private Vector3f      BBMax    = null;
    private Vector3f      BBMin    = null;
    private Vector3f      center   = null;

    private class Face {
        private int[] vertexID   = new int[3];
        private int[] texCoordID = new int[3];
        private int   textureID  = 0;
    }
    
    private class Mesh {
        private String      name        = "";
        private int[]       texture     = null;
        private Vector3f[]  vertex      = null;
        private Vector2f[]  texCoord    = null;
        private Face[]      face        = null;
        private int[]       facePerTex  = null;
        private IntBuffer   vboIndex    = null;
        private Vector3f    scale       = null;
        private Vector3f    translate   = null;
        private Vector3f    rotateAxis  = null;
        private float       rotateAngle = 0.0f;
        
        private ArrayList<Mesh> child = new ArrayList<>();
    }
    
    private void push(Mesh mesh) {
        mesh.vboIndex = BufferUtils.createIntBuffer(mesh.texture.length);
        glGenBuffersARB(mesh.vboIndex);
            
        for (int j = 0; j < mesh.texture.length; j++) {
            FloatBuffer fb = BufferUtils.createFloatBuffer(mesh.facePerTex[j] * 15);
            for (int k = 0; k < mesh.face.length; k++) {
                if (mesh.face[k].textureID != j) {
                    continue;
                }
                for (int l = 0; l < 3; l++) {
                    fb.put(mesh.vertex[mesh.face[k].vertexID[l]].x);
                    fb.put(mesh.vertex[mesh.face[k].vertexID[l]].y);
                    fb.put(mesh.vertex[mesh.face[k].vertexID[l]].z);
                    fb.put(mesh.texCoord[mesh.face[k].texCoordID[l]].x);
                    fb.put(mesh.texCoord[mesh.face[k].texCoordID[l]].y);
                }
            }
            fb.flip();

            glBindBufferARB(GL_ARRAY_BUFFER_ARB, mesh.vboIndex.get(j));
            glBufferDataARB(GL_ARRAY_BUFFER_ARB, fb, GL_STATIC_COPY_ARB);
        }
        
        for (int i = 0; i < mesh.child.size(); i++) {
            push(mesh.child.get(i));
        }
    }
    
    private void drawMesh(Mesh node) {
        for (int i = 0; i < node.texture.length; i++) {
            if (texture[node.texture[i]] == null) {
                continue;
            }
            texture[node.texture[i]].bind();
            glBindBufferARB(GL_ARRAY_BUFFER_ARB, node.vboIndex.get(i));
            glVertexPointer(3, GL_FLOAT, 20, 0);
            glTexCoordPointer(2, GL_FLOAT, 20, 12);
            glDrawArrays(GL_TRIANGLES, 0, node.facePerTex[i] * 3);
        }

        for (int i = 0; i < mesh[i].child.size(); i++) {
            drawMesh(mesh[i].child.get(i));
        }
    }
    
    public void draw() {
        /*glDisable(GL_TEXTURE_2D);
        glColor3f(1.0f, 0.0f, 0.0f);
        glBegin(GL_LINES);
            glVertex3f(BBMin.x, BBMin.y, BBMin.z);
            glVertex3f(BBMax.x, BBMin.y, BBMin.z);
            glVertex3f(BBMax.x, BBMax.y, BBMin.z);
            glVertex3f(BBMin.x, BBMax.y, BBMin.z);
            glVertex3f(BBMin.x, BBMin.y, BBMax.z);
            glVertex3f(BBMax.x, BBMin.y, BBMax.z);
            glVertex3f(BBMax.x, BBMax.y, BBMax.z);
            glVertex3f(BBMin.x, BBMax.y, BBMax.z);
            
            glVertex3f(BBMin.x, BBMin.y, BBMin.z);
            glVertex3f(BBMin.x, BBMin.y, BBMax.z);
            glVertex3f(BBMax.x, BBMin.y, BBMax.z);
            glVertex3f(BBMax.x, BBMin.y, BBMin.z);
            glVertex3f(BBMin.x, BBMax.y, BBMin.z);
            glVertex3f(BBMin.x, BBMax.y, BBMax.z);
            glVertex3f(BBMax.x, BBMax.y, BBMax.z);
            glVertex3f(BBMax.x, BBMax.y, BBMin.z);
            
            glVertex3f(BBMin.x, BBMin.y, BBMin.z);
            glVertex3f(BBMin.x, BBMax.y, BBMin.z);
            glVertex3f(BBMin.x, BBMax.y, BBMax.z);
            glVertex3f(BBMin.x, BBMin.y, BBMax.z);
            glVertex3f(BBMax.x, BBMin.y, BBMin.z);
            glVertex3f(BBMax.x, BBMax.y, BBMin.z);
            glVertex3f(BBMax.x, BBMax.y, BBMax.z);
            glVertex3f(BBMax.x, BBMin.y, BBMax.z);
        glEnd();
        glColor3f(1.0f, 1.0f, 1.0f);
        glEnable(GL_TEXTURE_2D);*/
        drawMesh(mesh[0]);
    }
    
    private void multMatrix(FloatBuffer matrix, Vector3f v, Vector3f vout) {
        float x = v.x * matrix.get(0) + v.y * matrix.get(4) + v.z * matrix.get(8) + matrix.get(12);
        float y = v.x * matrix.get(1) + v.y * matrix.get(5) + v.z * matrix.get(9) + matrix.get(13);
        float z = v.x * matrix.get(2) + v.y * matrix.get(6) + v.z * matrix.get(10) + matrix.get(14);
        
        vout.set(x, y, z);
    }
    
    private void multMatrix(Matrix4f matrix, Vector3f v) {
        float x = v.x * matrix.m00 + v.y * matrix.m10 + v.z * matrix.m20 + matrix.m30;
        float y = v.x * matrix.m01 + v.y * matrix.m11 + v.z * matrix.m21 + matrix.m31;
        float z = v.x * matrix.m02 + v.y * matrix.m12 + v.z * matrix.m22 + matrix.m32;
        
        v.set(x, y, z);
    }
    
    private void transformMesh(Mesh node, FloatBuffer transform) {
        Matrix4f m1 = new Matrix4f();
        Matrix4f m2 = new Matrix4f();
        m1.load(transform);
        
        m2.setIdentity();
        m2.scale(node.scale);
        if (node == mesh[0]) {
            if (mesh.length == 1) {
                m2.translate(new Vector3f(0.0f, center.y - BBMax.y, 0.0f));
                m2.rotate(node.rotateAngle, node.rotateAxis);
                m2.translate(new Vector3f(-center.x, -center.y, -center.z));
            } else {
                m2.translate(new Vector3f(-center.x, -BBMax.y, -center.z));
                m2.rotate(node.rotateAngle, node.rotateAxis);
            }
        } else {
            m2.translate(node.translate);
            m2.rotate(node.rotateAngle, node.rotateAxis);
        }
        
        Matrix4f.mul(m1, m2, m1);
        for (int i = 0; i < node.vertex.length; i++) {
            multMatrix(m1, node.vertex[i]);
        }
        
        transform.clear();
        m1.store(transform);
        for (int i = 0; i < node.child.size(); i++) {
            transformMesh(node.child.get(i), transform);
        }
    }
    
    private void getBoundingBox(Mesh node) {
        for (int i = 0; i < node.vertex.length; i++) {
            BBMax.x = Math.max(BBMax.x, node.vertex[i].x);
            BBMax.y = Math.max(BBMax.y, node.vertex[i].y);
            BBMax.z = Math.max(BBMax.z, node.vertex[i].z);
            BBMin.x = Math.min(BBMin.x, node.vertex[i].x);
            BBMin.y = Math.min(BBMin.y, node.vertex[i].y);
            BBMin.z = Math.min(BBMin.z, node.vertex[i].z);
        }
        for (int i = 0; i < node.child.size(); i++) {
            getBoundingBox(node.child.get(i));
        }
    }

    public boolean read(GRF grf, String fileName) {
        try {
            ByteBuffer bb = grf.getData("data\\model\\" + fileName);
            bb.order(ByteOrder.LITTLE_ENDIAN);

            // Check if input file is a valid RSM file by its signature/magic key
            byte[] tmpData = new byte[magicKey.length()];
            bb.get(tmpData, 0, magicKey.length());
            if (!magicKey.contentEquals(new String(tmpData))) {
                throw new Exception("Error: " + fileName + " is not a valid RSM file.");
            }

            version = bb.get() * 10 + bb.get();
            bb.position(bb.position() + 4 + 4 + 1); // Not needed
            bb.position(bb.position() + 16);        // Not needed

            // Textures
            texture = new BMP[bb.getInt()];
            String tmpName;
            for (int i = 0; i < texture.length; i++) {
                tmpName = Utility.read(bb, 0, 40);
                if (tmpName.endsWith(".bmp")) {
                    texture[i] = grf.getBMP("texture\\" + tmpName);
                } else {
                    texture[i] = null;
                    System.out.println("Can't open texture aside BMP files");
                }
            }
            
            // Meshes
            bb.position(bb.position() + 40); // Main node name
            mesh = new Mesh[bb.getInt()];

            FloatBuffer offset = BufferUtils.createFloatBuffer(16);
            for (int i = 0; i < mesh.length; i++) {
                mesh[i] = new Mesh();
                mesh[i].name = Utility.read(bb, 0, 40);
                String nodeParent = Utility.read(bb, 0, 40);
                if (!nodeParent.isEmpty()) {
                    for (int j = 1; j <= i; j++) {
                        if (mesh[j].name.equalsIgnoreCase(nodeParent)) {
                            mesh[j].child.add(mesh[i]);
                            break;
                        }
                    }
                }
                
                // Textures used in mesh
                mesh[i].texture = new int[bb.getInt()];
                for (int j = 0; j < mesh[i].texture.length; j++) {
                    mesh[i].texture[j] = bb.getInt();
                }

                // Transformation
                for (int j = 0; j < 4; j++) {
                    offset.put(bb.getFloat());
                    offset.put(bb.getFloat());
                    offset.put(bb.getFloat());
                    offset.put(j == 3 ? 1.0f : 0.0f);
                }
                offset.flip();
                
                mesh[i].translate   = new Vector3f(bb.getFloat(), bb.getFloat(), bb.getFloat());
                mesh[i].rotateAngle = bb.getFloat(); // in radian
                mesh[i].rotateAxis  = new Vector3f(bb.getFloat(), bb.getFloat(), bb.getFloat());
                mesh[i].scale       = new Vector3f(bb.getFloat(), bb.getFloat(), bb.getFloat());
                
                // Mesh vertices
                mesh[i].vertex = new Vector3f[bb.getInt()];
                for (int j = 0; j < mesh[i].vertex.length; j++) {
                    mesh[i].vertex[j] = new Vector3f(bb.getFloat(), bb.getFloat(), bb.getFloat());
                    multMatrix(offset, mesh[i].vertex[j], mesh[i].vertex[j]);
                }
                
                // Mesh UV Coord
                mesh[i].texCoord = new Vector2f[bb.getInt()];
                for (int j = 0; j < mesh[i].texCoord.length; j++) {
                    bb.position(bb.position() + 4); // Always zero
                    mesh[i].texCoord[j] = new Vector2f(bb.getFloat(), bb.getFloat());
                }
                
                // Faces in mesh
                mesh[i].face       = new Face[bb.getInt()];
                mesh[i].facePerTex = new int[texture.length];
                for (int j = 0; j < mesh[i].face.length; j++) {
                    mesh[i].face[j] = new Face();
                    mesh[i].face[j].vertexID[0] = bb.getShort();
                    mesh[i].face[j].vertexID[1] = bb.getShort();
                    mesh[i].face[j].vertexID[2] = bb.getShort();
                    mesh[i].face[j].texCoordID[0] = bb.getShort();
                    mesh[i].face[j].texCoordID[1] = bb.getShort();
                    mesh[i].face[j].texCoordID[2] = bb.getShort();
                    mesh[i].face[j].textureID = bb.getShort();
                    mesh[i].facePerTex[mesh[i].face[j].textureID]++;
                    
                    bb.position(bb.position() + 2 + 4 + 4); // Unused yet
                }
                
                // Animation position, for version 1.5 and above
                if (version >= 15) {
                    int nframe = bb.getInt();
                    for (int j = 0; j < nframe; j++) {
                        bb.position(bb.position() + 16);
                    }
                }
                
                // Animation rotation
                int nframe = bb.getInt();
                for (int j = 0; j < nframe; j++) {
                    bb.position(bb.position() + 20);
                }    
            }
            
            // Animation position, for version 1.5 below
            if (version < 15) {
                int nframe = bb.getInt();
                for (int i = 0; i < nframe; i++) {
                    bb.position(bb.position() + 16);
                }
            }
            
            // Volume box
            int nvol = bb.getInt();
            for (int i = 0; i < nvol; i++) {
                bb.position(bb.position() + 40);
            }
            
            // Get Bounding Box before transformation
            BBMax = new Vector3f(0.0f, 0.0f, 0.0f);
            BBMin = new Vector3f(0.0f, 0.0f, 0.0f);
            getBoundingBox(mesh[0]);
            center = new Vector3f((BBMax.x + BBMin.x) / 2.0f, (BBMax.x + BBMin.x) / 2.0f, (BBMax.z + BBMin.z) / 2.0f);
            
            // Transform mesh vertex
            FloatBuffer fb = BufferUtils.createFloatBuffer(16);
            Matrix4f    mx = new Matrix4f();
            mx.setIdentity();
            mx.store(fb);
            fb.flip();
            transformMesh(mesh[0], fb);
            
            // Get real Bounding Box
            BBMax.set(0.0f, 0.0f, 0.0f);
            BBMin.set(0.0f, 0.0f, 0.0f);
            getBoundingBox(mesh[0]);
            center.set((BBMax.x + BBMin.x) / 2.0f, (BBMax.x + BBMin.x) / 2.0f, (BBMax.z + BBMin.z) / 2.0f);
        } catch (Exception ex) {
            Logger.getLogger(RSM.class.getName()).log(Level.SEVERE, null, ex);
            return false;
        }
        
        push(mesh[0]); // Push all data into VBO
        return true;
    }

    public void destroy() {
        for (int i = 0; i < mesh.length; i++) {
            if (mesh[i].vboIndex != null) {
                glDeleteBuffersARB(mesh[i].vboIndex);
            }
        }
        for (int i = 0; i < texture.length; i++) {
            if (texture[i] == null) {
                continue;
            }
            texture[i].destroy();
        }
    }
    
    public Vector3f getBBMax() {
        return BBMax;
    }
    
    public Vector3f getBBMin() {
        return BBMin;
    }
}
